Build full-stack React apps with TanStack Start on Cloudflare Workers. Type-safe routing, server functions, SSR/streaming, D1/KV/R2 integration. Use when building full-stack React apps with SSR, migrating from Next.js, or from Vinxi to Vite (v1.121.0+). Prevents 9 documented errors including middleware bugs, file upload limitations, and deployment config issues.
⚠️ Status: Production Ready (RC v1.154.0)
TanStack Start is a full-stack React framework built on TanStack Router. It provides type-safe routing, server functions, SSR/streaming, and first-class Cloudflare Workers support.
Current Package: @tanstack/react-start@1.154.0 (Jan 21, 2026)
Production Readiness:
This skill prevents 9 documented errors and provides comprehensive guidance for Cloudflare Workers deployment, migrations, and server function patterns.
# Create new project (uses Vite)
npm create cloudflare@latest my-app -- --framework=tanstack-start
cd my-app
# Install dependencies
npm install
# Development
npm run dev
# Build and deploy
npm run build
wrangler deploy{
"dependencies": {
"@tanstack/react-start": "^1.154.0",
"@tanstack/react-router": "latest",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"vite": "latest",
"@cloudflare/vite-plugin": "latest",
"wrangler": "latest"
}
}Timeline: TanStack Start migrated from Vinxi to Vite in v1.121.0 (released June 10, 2025).
| Change | Old (Vinxi) | New (Vite) |
|---|---|---|
| Package name | @tanstack/start | @tanstack/react-start |
| Config file | app.config.ts | vite.config.ts |
| API routes | createAPIFileRoute() | createServerFileRoute().methods() |
| Entry files | ssr.tsx, client.tsx | server.tsx (optional) |
| Source folder | app/ | src/ |
| Dev command | vinxi dev | vite dev |
# 1. Remove Vinxi
npm uninstall vinxi @tanstack/start
# 2. Install Vite and framework-specific adapter
npm install vite @tanstack/react-start @cloudflare/vite-plugin
# 3. Delete old config
rm app.config.ts
# 4. Delete default entry files (unless customized)
rm app/ssr.tsx app/client.tsx
# 5. Rename customized entries
mv app/ssr.tsx app/server.tsx # If you customized SSR entry
# 6. Move source files (optional, for consistency)
mv app/ src/import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
tanstackStart(),
cloudflare({
viteEnvironment: { name: 'ssr' } // Required for Workers
})
]
}){
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"start": "node .output/server/index.mjs"
}
}// Old (Vinxi)
import { createAPIFileRoute } from '@tanstack/start/api'
export const Route = createAPIFileRoute('/api/users')({
GET: async () => {
return { users: [] }
}
})
// New (Vite)
import { createServerFileRoute } from '@tanstack/react-start/api'
export const Route = createServerFileRoute('/api/users').methods({
GET: async () => {
return { users: [] }
}
})Error: "invariant failed: could not find the nearest match"
Cause: Old Vinxi route definitions mixed with Vite config
Fix: Update all createAPIFileRoute() → createServerFileRoute().methods()
Error: "SyntaxError: The requested module '@tanstack/router-generator' does not provide an export named 'CONSTANTS'"
Cause: Conflicting Vinxi/Vite dependencies
Fix: Delete node_modules/, package-lock.json, reinstall
Issue: Auto-generated app.config.timestamp_* files duplicating
Cause: Old Vinxi config interfering
Fix: Delete all app.config.* files, restart dev server
Reference: Official Migration Guide | LogRocket Migration Article
name = "my-app"
compatibility_date = "2026-01-21"
compatibility_flags = ["nodejs_compat"] # REQUIRED
# REQUIRED: Point to TanStack Start's server entry
main = "@tanstack/react-start/server-entry"
[observability]
enabled = true # Optional: Enable monitoringimport { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
tanstackStart(),
cloudflare({
viteEnvironment: { name: 'ssr' } // REQUIRED
})
]
})# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
# KV Namespace
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"
# R2 Bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"Access bindings in server functions:
import { createServerFn } from '@tanstack/react-start/server'
export const getUser = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
// D1
const result = await env.DB.prepare('SELECT * FROM users').all()
// KV
const value = await env.KV.get('key')
// R2
const object = await env.BUCKET.get('file.txt')
return result.results
})Critical: Prerendering runs during build step using LOCAL environment variables, not Cloudflare bindings.
Problem: If routes use loaders that query D1/KV/R2, prerendering will fail because bindings aren't available at build time.
Solutions:
export const Route = createFileRoute('/users')({
loader: async () => {
// This route queries D1
},
// Disable prerendering
prerender: false
})wrangler dev running):# In CI environment
export CLOUDFLARE_INCLUDE_PROCESS_ENV=true
# Use .env file (NOT .env.local) for CI
# .env.local is gitignored and won't be in CIloader: async ({ context }) => {
// Skip DB queries during prerender
if (typeof context.cloudflare === 'undefined') {
return { users: [] }
}
const result = await context.cloudflare.env.DB.prepare('SELECT * FROM users').all()
return { users: result.results }
}Version Requirements:
@tanstack/react-start@1.138.0+Reference: Cloudflare Workers Guide
Server functions run on the server and can access Cloudflare bindings, databases, and secrets.
import { createServerFn } from '@tanstack/react-start/server'
export const getUsers = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const result = await env.DB.prepare('SELECT * FROM users').all()
return result.results
})import { getUsers } from './server-functions'
function UserList() {
const users = await getUsers()
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}⚠️ Known Issue: TanStack Start automatically calls await request.formData() for multipart/form-data requests, loading entire files into memory BEFORE the handler runs.
Impact:
Example of the Problem:
export const uploadFile = createServerFn()
.handler(async ({ request }) => {
// By the time this runs, the entire file is already in memory
const formData = await request.formData()
const file = formData.get('file') as File
// Too late to check size - file already loaded!
if (file.size > 10_000_000) {
throw new Error("File too large")
}
})Workarounds:
function FileUpload() {
const handleSubmit = async (e: FormEvent) => {
const file = e.currentTarget.querySelector('input[type="file"]').files[0]
if (file.size > 10_000_000) {
alert("File too large")
return
}
await uploadFile({ file })
}
return <form onSubmit={handleSubmit}>...</form>
}Status: Open issue #5704, no fix planned yet.
When a server function performs a redirect, the promise resolves to undefined instead of the declared return type.
const login = createServerFn<{ username: string, password: string }, User>()
.handler(async ({ data, request }) => {
const user = await authenticateUser(data)
if (!user) {
// Redirect returns void, but type says it returns User
throw redirect({ to: '/login', status: 401 })
}
return user
})
// In component
const result = await login({ username, password })
// result is undefined if redirected, User object otherwise
// Check before using!
if (result) {
console.log(result.name)
}Prevention: Always check return value before use if server function can redirect.
Status: Open PR #6295 to fix return type.
Problem: When using stateful backends, server functions lose auth context because requests originate from the Start server, not the browser. Cookies, CSRF tokens, and origin headers are missing.
// This FAILS - cookies not forwarded
const getData = createServerFn()
.handler(async () => {
const response = await fetch('https://api.example.com/user')
// 401 Unauthorized - no cookies!
})Solution 1: Use createIsomorphicFn (runs on client when possible)
import { createIsomorphicFn } from '@tanstack/react-start/server'
const getData = createIsomorphicFn()
.handler(async () => {
// Runs on client when possible, preserving cookies
const response = await fetch('https://api.example.com/user')
return response.json()
})Solution 2: Manual Header Forwarding
import { createServerFn } from '@tanstack/react-start/server'
import { getRequestHeaders } from '@tanstack/react-start/server'
const getData = createServerFn()
.handler(async () => {
const headers = getRequestHeaders() // Get browser's original headers
const response = await fetch('https://api.example.com/user', {
headers: {
'Cookie': headers.get('cookie') || '',
'X-XSRF-TOKEN': headers.get('x-xsrf-token') || '',
'Origin': headers.get('origin') || '',
}
})
return response.json()
})When to Use Each:
createIsomorphicFn: Best for read operations, maintains full browser contextReference: GitHub Discussion #6289
Issue: Better Auth cookie caching has edge cases with TanStack Start:
multiSession, lastLoginMethod, oneTap)Solution: Use Better Auth's official TanStack Start plugin
import { betterAuth } from 'better-auth'
import { reactStartCookies } from 'better-auth/plugins'
export const auth = betterAuth({
plugins: [
reactStartCookies(), // Handles cookie setting for TanStack Start
],
// ... other config
})Known Limitations:
References: Issue #4389, Issue #5639
Issue: Deploying with Prisma Edge fails with "No such module 'assets/.prisma/client/edge'" error.
Solution: Configure Prisma for Cloudflare runtime
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
engineType = "library"
runtime = "cloudflare" // or "workerd"
}Alternative Configuration:
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Then use with Cloudflare Hyperdrive:
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import { Pool } from 'pg'
export const getUser = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString })
const adapter = new PrismaPg(pool)
const prisma = new PrismaClient({ adapter })
return prisma.user.findMany()
})Reference: Cloudflare Workers SDK Issue #10969
export const getUsers = createServerFn()
.handler(async ({ request }) => {
const env = request.context.cloudflare.env
const result = await env.DB.prepare('SELECT * FROM users').all()
return result.results
})Use with drizzle-orm-d1 skill for type-safe ORM.
This skill prevents 9 documented issues:
Error: Errors thrown by server functions bypass middleware try-catch blocks Source: GitHub Issue #6381 Status: Fixed in v1.155+ (expected release)
Why It Happens: Server function errors are returned as error objects in the response, not thrown directly.
Prevention (workaround for v1.154 and earlier):
import { createMiddleware } from '@tanstack/react-start/server'
const middleware = createMiddleware().server(async (ctx) => {
try {
const r = await ctx.next()
// Check for error in response object
if ('error' in r && r.error) {
throw r.error
}
return r
} catch (error: any) {
console.error("Middleware caught an error:", error)
return new Response("An error occurred", { status: 500 })
}
})Error: Large file uploads consume excessive memory Source: GitHub Issue #5704 Status: Open, no fix planned
Why It Happens: Framework automatically calls await request.formData() before handler runs, loading entire file into memory.
Prevention:
See File Upload Limitation section for details.
Error: Type errors when using server function result after redirect Source: GitHub PR #6295 Status: Open PR
Why It Happens: Redirects return void, but return type doesn't reflect this.
Prevention: Always check server function return value before use
const result = await login({ username, password })
if (result) {
// Safe to use result
console.log(result.name)
}Error: 401 Unauthorized when calling stateful backend APIs from server functions Source: GitHub Discussion #6289
Why It Happens: Server functions originate from Start server, not browser, so cookies aren't forwarded.
Prevention: Use createIsomorphicFn or manual header forwarding
See Stateful Backend Integration section.
Error: "No such module 'assets/.prisma/client/edge'" Source: Cloudflare Workers SDK Issue #10969 Status: Resolved with runtime config
Why It Happens: Prisma Edge client not properly bundled for Workers environment.
Prevention: Configure Prisma with runtime = "cloudflare" in schema.prisma
See Prisma with Cloudflare Workers section.
Error: Session cookies not set/refreshed properly Source: Better Auth Issues #4389, #5639
Why It Happens: Better Auth's default cookie handling doesn't account for Start's execution model.
Prevention: Use reactStartCookies() plugin
See Better Auth Integration section.
Error: Runtime errors when using Node.js APIs on Cloudflare Workers Source: Cloudflare Workers Guide
Why It Happens: TanStack Start uses Node.js APIs that require compatibility flag.
Prevention: Add compatibility_flags = ["nodejs_compat"] to wrangler.toml
Error: Build fails when routes with loaders use D1/KV/R2 Source: Cloudflare Workers Guide
Why It Happens: Prerendering runs at build time without access to Cloudflare bindings.
Prevention: Disable prerendering for routes with bindings, or use conditional logic
See Prerendering Gotchas section.
Error: "invariant failed: could not find the nearest match" after upgrading to v1.121.0+ Source: Release v1.121.0
Why It Happens: v1.121.0 migrated from Vinxi to Vite with breaking changes.
Prevention: Follow complete migration guide
See Migration from Vinxi to Vite section.
Feature: Build-time replacement of process.env.NODE_ENV for better optimization (v1.154.0+)
// This condition is statically evaluated and dead code eliminated
if (process.env.NODE_ENV === 'production') {
// Production-only code
} else {
// Development-only code (removed in prod build)
}Automatic: No configuration needed, works out of the box.
Issue: Apps with 100+ routes generate 700+ HTTP requests in Vite dev mode.
Why: routeTree.gen.ts statically imports every route for type generation, even though autoCodeSplitting is enabled by default.
Impact: Slow dev server, hits proxy rate limits (ngrok 360 req/min)
Status: Expected behavior until Router v2. Not a bug, architectural limitation.
Workarounds:
Reference: GitHub Discussion #6353
Official Documentation:
Migration Guides:
Related Skills:
cloudflare-worker-base - Cloudflare Workers deployment patternsdrizzle-orm-d1 - Type-safe D1 database accessai-sdk-core - AI integration with server functionsreact-hook-form-zod - Form handling with validationLast verified: 2026-01-21 | Skill version: 2.0.0 | Changes: Expanded from draft with 9 documented issues, migration guide, Cloudflare deployment, auth patterns, and database integration
fa91c34
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.